--
-- Hunter shot prediction module
--

-- '_G' stuff is to keep lualint happy

if _G['select'](2, _G['UnitClass']('player')) ~= 'HUNTER' then return end

-------------------------------------------
-- Local references to globals for speed and lint's happiness
-------------------------------------------
local _G = _G
local Rotor = _G['Rotor']
local GetSpellInfo = _G['GetSpellInfo']
local UnitRangedDamage = _G['UnitRangedDamage']
local UnitHealth = _G['UnitHealth']
local GetTime = _G['GetTime']
local strsplit = _G['strsplit']
local IsUsableSpell = _G['IsUsableSpell']
local UnitPowerMax = _G['UnitPowerMax']
local GetPowerRegen = _G['GetPowerRegen']
local GetPrimaryTalentTree = _G['GetPrimaryTalentTree']
local UnitLevel = _G['UnitLevel']
local DEFAULT_CHAT_FRAME = _G['DEFAULT_CHAT_FRAME']
local CreateFrame = _G['CreateFrame']
local WorldFrame = _G['WorldFrame']
local format = _G['format']
local GetNumTalents = _G['GetNumTalents']
local GetTalentInfo = _G['GetTalentInfo']
local max = _G['max']
local UnitGUID = _G['UnitGUID']

---
-- Debug
---
local DebugSends = true
local DebugFails = false
local DebugSuggestions = true


-------------------------------------------
-- Constants
-------------------------------------------
local MODULE = 'Hunter'

--
-- Shots ids
--
local ID_AIS        = 19434 -- aimed shot
local ID_INST_AIS   = 82928 -- aimed shot!
local ID_AS         = 3044  -- arcane shot
local ID_KS         = 53351 -- kill shot
local ID_SS         = 56641 -- steady shot
local ID_SRS        = 1978  -- serpent sting
local ID_CS         = 53209 -- chimera shot
local ID_ES         = 53301 -- explosive shot
local ID_BA         = 3674  -- black arrow
local ID_MS         = 2643  -- multi shot
local ID_KC         = 34026 -- kill command
local ID_COS        = 77767 -- cobra shot
local ID_RF         = 3045  -- rapid fire
local ID_RD         = 23989 -- readiness
local ID_BW         = 19574 -- bestial wrath
local ID_FD         = 5384  -- fake death
local ID_MD         = 34477 -- misdirection
local ID_HM         = 1130  -- hunter's mark
local ID_AUS        = 75    -- autoshot
local ID_MEND_PET   = 136   -- mend pet
local ID_DISENGAGE = 781   -- disengage
local ID_COW        = 53434 -- call of the wild
local ID_DETERRENCE = 19263 -- deterrence

--
-- Buff ids
--
local ID_MMM                = 82926 -- master marksman
local ID_ISS                = 53224 -- improved steady shot
local ID_LNL                = 56453 -- lock'n'load
local ID_TBW                = 34692 -- the beast within (buff and talent)
local ID_KIS                = 94007 -- killing streak
local ID_BURNING_ADRENALINE = 99060 -- t12 4p bonus buff (Burning Adrenaline)

--
-- talent ids
--
local ID_EFF_TAL    = 19416 -- efficiency talent
local ID_RFUL_TAL   = 34491 -- Resourcefullness
local ID_CAIM_TAL   = 34482 -- Careful Aim

--
-- glyph ids
--
local ID_CS_GLYPH   = 63065 -- Glyph of Chimera Shot
local ID_KS_GLYPH   = 63067 -- Glyph of Kill Shot
local ID_KC_GLYPH   = 56842 -- Glyph of Kill Command

-- battle aspect ids
local ID_ASPECT_OF_THE_HAWK = 13165
local ID_ASPECT_OF_THE_FOX = 82661
local BATTLE_ASPECT_IDS = {
    ID_ASPECT_OF_THE_HAWK,
    ID_ASPECT_OF_THE_FOX,
    20043 -- aspect of the wild
}

--
-- Spell names
--
local SS_NAME = Rotor.spell_name_cache[ID_SS]
local AUTOSHOT_NAME = Rotor.spell_name_cache[ID_AUS]
local AIS_NAME = Rotor.spell_name_cache[ID_AIS]
local INST_AIS_NAME = Rotor.spell_name_cache[ID_INST_AIS]
local COS_NAME = Rotor.spell_name_cache[ID_COS]
local ES_NAME = Rotor.spell_name_cache[ID_ES]

local options

local IsMM = false
local IsSV = false
local IsBM = false

local IssTal = 0    -- Improved Steady Shot rank
local RfulTal = 0   -- Resourcefullness rank
local CaimTal = 0   -- Careful Aim rank

local HaveCsGlyph = false
local HaveKsGlyph = false
local HaveKcGlyph = false

-- Rotation config vars
local as_in_ca_zone = false
local handle_ca_zone = true

-- The module
local Mod = Rotor:NewModule(MODULE)

--
-- Utilites
--

local function GetTalents()
    IssTal = Rotor.talents[Rotor.spell_name_cache[ID_ISS]]
    RfulTal = Rotor.talents[Rotor.spell_name_cache[ID_RFUL_TAL]]
    CaimTal = Rotor.talents[Rotor.spell_name_cache[ID_CAIM_TAL]]
end

local function GetGlyphs()
    HaveKcGlyph = Rotor.glyphs[ID_KC_GLYPH] or false
    HaveKsGlyph = Rotor.glyphs[ID_KS_GLYPH] or false
    HaveCsGlyph = Rotor.glyphs[ID_CS_GLYPH] or false
end

local function GetSpellCost(spell)
    if type(spell) == 'number' then
        spell = Rotor.spell_name_cache[spell]
    end
    local _, _, _, cost = GetSpellInfo(spell)
    return cost
end

---------------------------------------------------------
--
-- Shot prediction
--

-- Spell objects
local cs        -- chimera shot
local ais       -- aimed shot
local ks        -- kill shot
local es        -- explosive shot
local ba        -- black arrow
local kc        -- kill command
local srs       -- serpent sting
local as        -- arcane shot

-- Buff objects
local mmm_buff  -- master marksman
local iss_buff  -- improved steady shot
local tbw_buff  -- the beast withing
local lnl_buff  -- lock'n'load
local kis_buff  -- killing streak
local rf_buff   -- rapid fire

-- Debuff objects
local srs_debuff
local hm_debuff
local es_debuff
local ba_debuff

-- Other vars
local main_shot

local regen_shot_id

local maxPower = UnitPowerMax('player')

local myGUID = UnitGUID('player')

local battle_aspect_buffs

--
-- Shot prediciton vars
--
local last_sent = ''
local prev_sent = ''
local last_double_ss_time = 0
local last_suggested = 0
local power_delta = 0
local last_avail_power = 0
local avail_power = 0
local gcd = 0
local now = GetTime()
local target_hp = 0
local cost_divider = 1
local main_shot_cost = 0
local focus_gain_mult = 0

local function ClearCachedData()
    last_sent = ''
    prev_sent = ''
    last_double_ss_time = 0

    power_delta = 0
    last_avail_power = 0
    last_suggested = 0
    focus_gain_mult = 0
end

ClearCachedData() -- initial cache setup

--
-- Methods implementation
--

function Mod:RegisterAuras()
    srs_debuff = Rotor:RegisterDebuff(ID_SRS, true)
    hm_debuff = Rotor:RegisterDebuff(ID_HM, false, 2, 'reminder')

    if IsSV then
        es_debuff = Rotor:RegisterDebuff(ID_ES, true)
        lnl_buff = Rotor:RegisterBuff(ID_LNL)
        ba_debuff = Rotor:RegisterDebuff(ID_BA, true)
    else
        es_debuff = nil
        lnl_buff = nil
        ba_debuff = nil
    end

    iss_buff = nil
    if IsMM then
        mmm_buff = Rotor:RegisterBuff(ID_MMM)
        if IssTal > 0 then
            iss_buff = Rotor:RegisterBuff(ID_ISS)
        end
    else
        mmm_buff = nil
    end

    -- Bestial Wrath
    if IsBM then
        tbw_buff = Rotor:RegisterBuff(ID_TBW)
        kis_buff = Rotor:RegisterBuff(ID_KIS)
    else
        tbw_buff = nil
        kis_buff = nil
    end

    -- Misdirection
    --Rotor:RegisterBuff(ID_MD, false, false, 3)

    -- Rapid Fire
    --rf_buff = Rotor:RegisterSelfBuff(ID_RF, 1)

    -- blood lust etc
    Rotor:initBlIndicator(4)

    -- aspects
    battle_aspect_buffs = {}
    for _, aspect_id in pairs(BATTLE_ASPECT_IDS) do
        table.insert(battle_aspect_buffs, Rotor:RegisterBuff(aspect_id))
    end

    -- Mend pet
    Rotor:RegisterPetBuff(ID_MEND_PET, false, 12)

    -- AoTF
    Rotor:RegisterSelfBuff(ID_ASPECT_OF_THE_FOX, 5)

    -- Deterrence
    Rotor:RegisterSelfBuff(ID_DETERRENCE, 11)
end

local common_spell_defs = {
            {id = ID_FD,            icon_pos = 9,   icon_type = 'reminder'},
            {id = ID_MD,            icon_pos = 8,                           with_buff = true},
    rf =    {id = ID_RD,            icon_pos = 7,   icon_type = 'reminder'},
            {id = ID_DISENGAGE,     icon_pos = 10,  icon_type = 'reminder'},
            {id = ID_COW,           icon_pos = 6,   icon_type = 'reminder', with_buff = true, is_pet_spell = true},
    ks =    {id = ID_KS},
    kc =    {id = ID_KC},
    as =    {id = ID_AS},
    srs =   {id = ID_SRS,                                                   with_debuff = true},
            {id = ID_COS,                                                                       must_exists = true}
}
local mm_spell_defs = {
    cs =    {id = ID_CS, must_exists = true},
    ais =   {id = ID_AIS}
}
local sv_spell_defs = {
    es =    {id = ID_ES, with_debuff = true},
    ba =    {id = ID_BA, with_debuff = true, must_exists = true},
}
local bm_spell_defs = {
}

function Mod:RegisterSpells()
    local d
    -- Feign Death
    Rotor:RegisterSpell(ID_FD, 9, 'reminder')

    -- Misdirection
    Rotor:RegisterSpellWithSelfBuff(ID_MD, 8)

    -- Rapid Fire
    d, rf_buff = Rotor:RegisterSpellWithSelfBuff(ID_RF, 7, 'reminder')

    Rotor:RegisterSpell(ID_DISENGAGE, 10, 'reminder')
    Rotor:RegisterSpellWithSelfBuff(ID_COW, 6, 'reminder').is_pet_spell = true

--[[
    -- Readiness
    if Rotor:HasSpell(ID_RD) then
        Rotor:RegisterSpell(ID_RD)
    end


    -- Bestial wrath
    if Rotor:HasSpell(ID_BW) then
        Rotor:RegisterSpell(ID_BW)
    end
]]
    -- cooldowns to track
    ks = Rotor:RegisterSpell(ID_KS)
    kc = Rotor:RegisterSpell(ID_KC)

    cs = nil
    es = nil
    ba = nil
    if IsMM then
        if Rotor:HasSpell(ID_CS) then
            cs = Rotor:RegisterSpell(ID_CS)
        end
        ais = Rotor:RegisterSpell(ID_AIS)
    elseif IsSV then
        if Rotor:HasSpell(ID_ES) then
            es = Rotor:RegisterSpell(ID_ES)
        end
        if Rotor:HasSpell(ID_BA) then
            ba = Rotor:RegisterSpell(ID_BA)
        end
    end

    -- add structs to track power delta and send/finish times
    as = Rotor:RegisterSpell(ID_AS)
    srs = Rotor:RegisterSpell(ID_SRS)
    Rotor:RegisterSpell(regen_shot_id) -- to watch in CastSent/CastFinished
end

function Mod:Init(opts)
    if not Rotor:HasSpell(ID_KC) then -- check for kill command
        return nil, 'unsupported toon level'
    end

    IsBM = Rotor.spec == 1
    IsMM = Rotor.spec == 2
    IsSV = Rotor.spec == 3

    options = opts

    if (UnitLevel('player') > 80) and not IsMM then
        regen_shot_id = ID_COS
    else
        regen_shot_id = ID_SS
    end

    do--if not Rotor.cfg.silent then
        if IsMM then
            DEFAULT_CHAT_FRAME:AddMessage('Using MM rotation')
        elseif IsSV then
            DEFAULT_CHAT_FRAME:AddMessage('Using SV rotation')
        else
            DEFAULT_CHAT_FRAME:AddMessage('Using BM rotation')
        end
    end

    GetTalents()
    GetGlyphs()

    maxPower = UnitPowerMax('player')

    self:RegisterAuras()

--[[for debug icons:

    for i = 1, 12 do
        Rotor:RegisterDebuff(50000+i, false, i, 'reminder')
    end
]]

    self:RegisterSpells()

    -- init additional spell info
    for k, v in pairs(Rotor.spells) do
        v.cost = GetSpellCost(k)
        v.fullCooldown = 0
        if k == ID_CS then
            if HaveCsGlyph then
                v.fullCooldown = 9
            else
                v.fullCooldown = 10
            end
        elseif k == ID_KC then
            v.fullCooldown = 6
        elseif k == ID_ES then
            v.fullCooldown = 6
        elseif k == ID_BA then
            v.fullCooldown = 30 - RfulTal * 2
        end
    end

    if IsMM then
        main_shot = cs
    elseif IsSV then
        main_shot = es
    else
        main_shot = kc
    end

    ClearCachedData()

    Rotor:showPowerDisplay()

    --Rotor:showUpperText() --``
    local spec_names = {'BM', 'MM', 'SV'}
    Rotor:print('Hunter module initialized ('..spec_names[Rotor.spec]..')')

    return true
end

-- cast handling
function Mod:CastSent(spell)
    if spell == AUTOSHOT_NAME then
        return false
    end

    local now = GetTime()
    if last_sent ~= '' then
        prev_sent = last_sent
    end
    last_sent = spell
    if last_sent == SS_NAME and prev_sent == SS_NAME then
        last_double_ss_time = now
    end

    if (spell == COS_NAME) or (spell == SS_NAME) then
        focus_gain_mult = focus_gain_mult + 1
    end

    local rec = Rotor.spells[Rotor.spell_id_cache[spell]]
    if rec then
        if (rec.id ~= ID_ES) or not lnl_buff.active then
            power_delta = rec.cost
        end
        if rec.id == last_suggested then
            last_suggested = 0
        end
    end
    if DebugSends and Rotor.cfg.debug then Rotor:dprint(Rotor.power, 'sent', spell) end
    return true
end

function Mod:CastFinished(spell, succ, fail_reason)
    if spell == AUTOSHOT_NAME then
        return true
    end

    if succ or (fail_reason == 'INTERRUPTED') then
        if spell == last_sent then
            power_delta = 0
        end
    end

    if not succ then
        if (spell == COS_NAME) or (spell == SS_NAME) then
            focus_gain_mult = max(0, focus_gain_mult - 1)
        end
        if (spell == last_sent)  then
            if last_sent == SS_NAME and prev_sent == SS_NAME then
                last_double_ss_time = 0
            end
            last_sent = '' -- fail send
        end
        if DebugFails and Rotor.cfg.debug then Rotor:dprint(Rotor.power, 'failed ('..fail_reason..')', spell) end
    elseif DebugSends and Rotor.cfg.debug then
        Rotor:dprint(Rotor.power, 'done', spell)
    end
end

--
-- Shot prediction
--
local function CalcAvailPower(power)
    local cur_fps = GetPowerRegen()
    local future_power = power - power_delta
    local power_till_main_shot

    --Rotor:setUpperText(focus_gain_mult) --``

    if (GetTime() - main_shot.sentAt) < 0.8 then
        power_till_main_shot = cur_fps * main_shot.fullCooldown
    else
        power_till_main_shot = cur_fps * main_shot.cd
    end

    local avail_power = math.floor(future_power + power_till_main_shot - main_shot_cost + 0.5)

    if focus_gain_mult > 0 then
        avail_power = avail_power + 18
        --if focus_gain_mult > 1 then
        --    avail_power = avail_power + 18
        --end
    end

    if avail_power < 0 then
        avail_power = 0
    end

    local diff = avail_power - last_avail_power
    if diff >= -3 and diff <= 3 then
        avail_power = last_avail_power -- stabilize analyzing
    else
        last_avail_power = avail_power
    end

    return avail_power
end

local function RemainSpellcastTime(ignore_ss)
    if (Rotor.spellcast_end_time > now) then
        if not ignore_ss or (last_sent ~= SS_NAME) then
            return Rotor.spellcast_end_time - now
        else
            return 0
        end
    else
        return 0
    end
end

local MIN_TARGET_HP_FOR_SRS = 200000
local MIN_TARGET_HP_FOR_AIS_ON_CA = 200000

local function IsUnsent(spell, time_since_last_sent)
    return (spell.sentAt <= spell.doneAt) and (spell.doneAt + (time_since_last_sent or 0.5) < now)
end

local function EnoughFocus(spell, extra_focus)
    return (spell.cost / cost_divider <= (avail_power + (extra_focus or 0)))
end

local function IsOffCdAndUnsent(spell, time_since_last_sent, wth)
    return ((spell.cd - (wth or 0.4)) <= gcd) and IsUnsent(spell, time_since_last_sent)
end

local function CheckKS()
    return ks.usable and IsOffCdAndUnsent(ks, nil, 0.2)
end

local function CheckSrS(dontCheckPower)
    return (IsSV or ((not Rotor.target_is_player) and ((target_hp > MIN_TARGET_HP_FOR_SRS) or (Rotor.target_max_hp == 1))))
        and (srs_debuff.time_left == 0)
        and (dontCheckPower or EnoughFocus(srs))
        and IsUnsent(srs, 3)
end

local function CheckAS()
    return EnoughFocus(as)
end

local function CheckCS()
    return IsOffCdAndUnsent(cs)
end

local function CheckES()
    return lnl_buff.active or IsOffCdAndUnsent(es)
end

local function CheckBA()
    local escd
    if (GetTime() - es.sentAt) < 0.5 then
        escd = es.fullCooldown
    else
        escd = es.cd
    end

    return IsOffCdAndUnsent(ba) and EnoughFocus(ba, math.floor(max(escd - gcd - 1, 0) / 1.8) * 18)
end

local function SvCheckAS()
    return ((ba.cd > 5) and EnoughFocus(as)) or (ba.cd <= 5 and EnoughFocus(ba))
    --return EnoughFocus(ba)
end

local function CheckKC()
    return IsOffCdAndUnsent(kc)
end

local function IsBattleAspectUp()
    for _, aspect_buff in pairs(battle_aspect_buffs) do
        if aspect_buff.active then
            return true
        end
    end
    return false
end

local function NextBmSpell()
    if CheckKS() then
        return ID_KS
    elseif CheckKC() then
        return ID_KC
    elseif CheckSrS() then
        return ID_SRS
    elseif CheckAS() then
        return ID_AS
    else
        return regen_shot_id
    end
end

function Mod:NextSkill(gcd_arg, cur_spell)
    -- check aspect
    if not IsBattleAspectUp() then
        return ID_ASPECT_OF_THE_HAWK
    end

    now = GetTime()

    gcd = max(max(gcd_arg, RemainSpellcastTime()), 0)

    target_hp = UnitHealth('target')

    local ca_zone = handle_ca_zone and (target_hp / Rotor.target_max_hp > 0.9) and (CaimTal > 0) and (Rotor.target_max_hp > MIN_TARGET_HP_FOR_AIS_ON_CA)

    cost_divider = 1
    if tbw_buff and tbw_buff.time_left > gcd then
        cost_divider = 2
    end

    if main_shot and not main_shot.cost then
        -- fix around bug where spell cost is not available right after respec
        main_shot.cost = GetSpellCost(main_shot.name)
        if not main_shot.cost then
            return 0
        end
    end

    if main_shot then
        main_shot_cost = main_shot.cost / cost_divider
    else
        main_shot_cost = 0
    end

    if kis_buff and kis_buff.active then
        main_shot_cost = main_shot_cost - 10
    end

    if main_shot then
        avail_power = CalcAvailPower(Rotor.power)
    else
        avail_power = Rotor.power
    end

    local next_spell_id

    if IsSV then
        if CheckSrS(true) then
            next_spell_id = ID_SRS
        elseif CheckES() and not (prev_sent == ES_NAME and last_sent == ES_NAME) then
            next_spell_id = ID_ES
        elseif (target_hp / Rotor.target_max_hp < 0.2) and IsOffCdAndUnsent(ks) then
            next_spell_id = ID_KS
        elseif CheckBA() then
            next_spell_id = ID_BA
        elseif (last_suggested == ID_COS or last_suggested == ID_AS) then
            next_spell_id = last_suggested
        elseif SvCheckAS() then
            next_spell_id = ID_AS
        else
            next_spell_id = regen_shot_id
        end
    elseif IsMM then
        if not Rotor.in_combat and cs and IsUnsent(cs, 3) and (cs.cd <= gcd) and (cs.cost <= Rotor.power) then
            return ID_CS
        elseif CheckKS() then
            next_spell_id = ID_KS
        elseif not cs then
            -- low level mm
            if Rotor.power >= as.cost then
                next_spell_id = ID_AS
            else
                next_spell_id = ID_SS
            end
        elseif not ca_zone and CheckCS() then
            next_spell_id = ID_CS
        elseif iss_buff and (iss_buff.time_left <= 4) and (now - last_double_ss_time) > 2 then
            -- force ISS buff
            next_spell_id = ID_SS
        elseif (mmm_buff.time_left > max(gcd, RemainSpellcastTime(false))) and (last_sent ~= INST_AIS_NAME) then
            next_spell_id = ID_AIS
        elseif not ca_zone and CheckSrS() then
            next_spell_id = ID_SRS
        elseif ca_zone and ((Rotor.power + GetPowerRegen() * max(1, RemainSpellcastTime(false)) - power_delta) > 46) then
            next_spell_id = ID_AIS
        elseif (not ca_zone or as_in_ca_zone) and CheckAS() then
            next_spell_id = ID_AS
        else
            next_spell_id = ID_SS
        end
    else -- BM
        next_spell_id = NextBmSpell()
    end

    if DebugSuggestions and (next_spell_id ~= last_suggested) and Rotor.cfg.debug then
        Rotor:dprint('SUG', Rotor.spell_name_cache[next_spell_id], Rotor.power, last_avail_power, power_delta, format('gcd:%.2f mscd:%.2f', gcd, main_shot.cd))
    end

    last_suggested = next_spell_id
	return next_spell_id
end

-- Command line handler
function Mod:HandleCommandLine(cmd)
    local cmd, args = strsplit(' ', (cmd or ''):lower(), 2)

    if cmd == 'hm' then
        if options.hunters_mark == true then
            options.hunters_mark = false
            print(MODULE .. ': disabled suggesting hunter\'s mark')
        else
            options.hunters_mark = true
            print(MODULE .. ': enabled suggesting hunter\'s mark')
        end
    else
        print(MODULE .. ': Options:')
        print(MODULE .. ': hm [option] - toggle hunter mark suggestion')
    end
end

function Mod:Detect()
    return Rotor:HasSpell(ID_KC) -- minimum req - kill command
end

function Mod:CombatStateChanged()
    if not Rotor.in_combat then
        -- clean up after combat
        ClearCachedData()
        focus_gain_mult = 0
        last_suggested = 0
    end
end

function Mod:TargetChanged(old_target)
    last_suggested = 0
end

function Mod:CheckUsable(spell_id)
    if (spell_id == ID_AIS) and mmm_buff.active then
        return true
    else
        return IsUsableSpell(spell_id) == 1
    end
end

function Mod:CheckInRange(spell_id)
    if spell_id == ID_HM then
        -- replace HM with Autoshot when checking range
        spell_id = ID_AUS
    end
    return Rotor:defaultRangeCheck(spell_id)
end

function Mod:HandleUnfilteredCombatEvent(_, _, _, srcGUID, _, _, _, _, _, _, _, spellID)
    if (spellID == 91954) and (srcGUID == myGUID) then  -- energize from cs/ss
        focus_gain_mult = max(0, focus_gain_mult - 1)
    end
end
